<?php

/**
 * Rights authorizer component class file.
 *
 * @author Christoffer Niska <cniska@live.com>
 * @copyright Copyright &copy; 2010 Christoffer Niska
 * @since 0.5
 */
class RAuthorizer extends CApplicationComponent {

    /**
     * @property string the name of the superuser role.
     */
    public $superuserName;

    /**
     * @property RDbAuthManager the authorization manager.
     */
    private $_authManager;

    /**
     * Initializes the authorizer.
     */
    public function init() {
        parent::init();

        $this->_authManager = Yii::app()->getAuthManager();
    }

    /**
     * Returns the a list of all roles.
     * @param boolean $includeSuperuser whether to include the superuser.
     * @param boolean $sort whether to sort the items by their weights.
     * @return the roles.
     */
    public function getRoles($includeSuperuser = true, $sort = true) {
        $exclude = $includeSuperuser === false ? array($this->superuserName) : array();
        $roles = $this->getAuthItems(CAuthItem::TYPE_ROLE, null, null, $sort, $exclude);
        $roles = $this->attachAuthItemBehavior($roles);
        return $roles;
    }

    /**
     * Creates an authorization item.
     * @param string $name the item name. This must be a unique identifier.
     * @param integer $type the item type (0: operation, 1: task, 2: role).
     * @param string $description the description for the item.
     * @param string $bizRule business rule associated with the item. This is a piece of
     * PHP code that will be executed when {@link checkAccess} is called for the item.
     * @param mixed $data additional data associated with the item.
     * @return CAuthItem the authorization item
     */
    public function createAuthItem($name, $type, $description = '', $bizRule = null, $data = null) {
        $bizRule = $bizRule !== '' ? $bizRule : null;

        if ($data !== null)
            $data = $data !== '' ? $this->sanitizeExpression($data . ';') : null;

        return $this->_authManager->createAuthItem($name, $type, $description, $bizRule, $data);
    }

    /**
     * Updates an authorization item.
     * @param string $oldName the item name. This must be a unique identifier.
     * @param integer $name the item type (0: operation, 1: task, 2: role).
     * @param string $description the description for the item.
     * @param string $bizRule business rule associated with the item. This is a piece of
     * PHP code that will be executed when {@link checkAccess} is called for the item.
     * @param mixed $data additional data associated with the item.
     */
    public function updateAuthItem($oldName, $name, $description = '', $bizRule = null, $data = null) {
        $authItem = $this->_authManager->getAuthItem($oldName);
        $authItem->name = $name;
        $authItem->description = $description !== '' ? $description : null;
        $authItem->bizRule = $bizRule !== '' ? $bizRule : null;

        // Make sure that data is not already serialized.
        if (@unserialize($data) === false)
            $authItem->data = $data !== '' ? $this->sanitizeExpression($data . ';') : null;

        $this->_authManager->saveAuthItem($authItem, $oldName);
    }

    /**
     * Returns the authorization items of the specific type and user.
     * @param mixed $types the item type (0: operation, 1: task, 2: role). Defaults to null,
     * meaning returning all items regardless of their type.
     * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if
     * they are not assigned to a user.
     * @param CAuthItem $parent the item for which to get the select options.
     * @param boolean $sort sort items by to weights.
     * @param array $exclude the items to be excluded.
     * @return array the authorization items of the specific type.
     */
    public function getAuthItems($types = null, $userId = null, CAuthItem $parent = null, $sort = true, $exclude = array()) {
        // We have none or a single type.
        if ($types !== (array) $types) {
            $items = $this->_authManager->getAuthItems($types, $userId, $sort);
        }
        // We have multiple types.
        else {
            $typeItemList = array();
            foreach ($types as $type)
                $typeItemList[$type] = $this->_authManager->getAuthItems($type, $userId, $sort);

            // Merge the authorization items preserving the keys.
            $items = array();
            foreach ($typeItemList as $typeItems)
                $items = $this->mergeAuthItems($items, $typeItems);
        }

        $items = $this->excludeInvalidAuthItems($items, $parent, $exclude);
        $items = $this->attachAuthItemBehavior($items, $userId, $parent);

        return $items;
    }

    /**
     * Merges two arrays with authorization items preserving the keys.
     * @param array $array1 the items to merge to.
     * @param array $array2 the items to merge from.
     * @return array the merged items.
     */
    protected function mergeAuthItems($array1, $array2) {
        foreach ($array2 as $itemName => $item)
            if (isset($array1[$itemName]) === false)
                $array1[$itemName] = $item;

        return $array1;
    }

    /**
     * Excludes invalid authorization items.
     * When an item is provided its parents and children are excluded aswell.
     * @param array $items the authorization items to process.
     * @param CAuthItem $parent the item to check valid authorization items for.
     * @param array $exclude additional items to be excluded.
     * @return array valid authorization items.
     */
    protected function excludeInvalidAuthItems($items, CAuthItem $parent = null, $exclude = array()) {
        // We are getting authorization items valid for a certain item
        // exclude its parents and children aswell.
        if ($parent !== null) {
            $exclude[] = $parent->name;
            foreach ($parent->getChildren() as $childName => $child)
                $exclude[] = $childName;

            // Exclude the parents recursively to avoid inheritance loops.
            $parentNames = array_keys($this->getAuthItemParents($parent->name));
            $exclude = array_merge($parentNames, $exclude);
        }

        // Unset the items that are supposed to be excluded.
        foreach ($exclude as $itemName)
            if (isset($items[$itemName]))
                unset($items[$itemName]);

        return $items;
    }

    /**
     * Returns the parents of the specified authorization item.
     * @param mixed $item the item name for which to get its parents.
     * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null,
     * meaning returning all items regardless of their type.
     * @param string $parentName the name of the item in which permissions to search.
     * @param boolean $direct whether we want the specified items parent or all parents.
     * @return array the names of the parent items.
     */
    public function getAuthItemParents($item, $type = null, $parentName = null, $direct = false) {
        if (($item instanceof CAuthItem) === false)
            $item = $this->_authManager->getAuthItem($item);

        $permissions = $this->getPermissions($parentName);
        $parentNames = $this->getAuthItemParentsRecursive($item->name, $permissions, $direct);
        $parents = $this->_authManager->getAuthItemsByNames($parentNames);
        $parents = $this->attachAuthItemBehavior($parents, null, $item);

        if ($type !== null)
            foreach ($parents as $parentName => $parent)
                if ((int) $parent->type !== $type)
                    unset($parents[$parentName]);

        return $parents;
    }

    /**
     * Returns the parents of the specified authorization item recursively.
     * @param string $itemName the item name for which to get its parents.
     * @param array $items the items to process.
     * @param boolean $direct whether we want the specified items parent or all parents.
     * @return the names of the parents items recursively.
     */
    private function getAuthItemParentsRecursive($itemName, $items, $direct) {
        $parents = array();
        foreach ($items as $childName => $children) {
            if ($children !== array()) {
                if (isset($children[$itemName])) {
                    if (isset($parents[$childName]) === false)
                        $parents[$childName] = $childName;
                }
                else {
                    if (($p = $this->getAuthItemParentsRecursive($itemName, $children, $direct)) !== array()) {
                        if ($direct === false && isset($parents[$childName]) === false)
                            $parents[$childName] = $childName;

                        $parents = array_merge($parents, $p);
                    }
                }
            }
        }

        return $parents;
    }

    /**
     * Returns the children for the specified authorization item recursively.
     * @param mixed $item the item for which to get its children.
     * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null,
     * meaning returning all items regardless of their type.
     * @return array the names of the item's children.
     */
    public function getAuthItemChildren($item, $type = null) {
        if (($item instanceof CAuthItem) === false)
            $item = $this->_authManager->getAuthItem($item);

        $childrenNames = array();
        foreach ($item->getChildren() as $childName => $child)
            if ($type === null || (int) $child->type === $type)
                $childrenNames[] = $childName;

        $children = $this->_authManager->getAuthItemsByNames($childrenNames);
        $children = $this->attachAuthItemBehavior($children, null, $item);

        return $children;
    }

    /**
     * Attaches the rights authorization item behavior to the given item.
     * @param mixed $items the item or items to which attach the behavior.
     * @param int $userId the ID of the user to which the item is assigned.
     * @param CAuthItem $parent the parent of the given item.
     * @return mixed the item or items with the behavior attached.
     */
    public function attachAuthItemBehavior($items, $userId = null, CAuthItem $parent = null) {
        // We have a single item.
        if ($items instanceof CAuthItem) {
            $items->attachBehavior('rights', new RAuthItemBehavior($userId, $parent));
        }
        // We have multiple items.
        else if ($items === (array) $items) {
            foreach ($items as $item)
                $item->attachBehavior('rights', new RAuthItemBehavior($userId, $parent));
        }

        return $items;
    }

    /**
     * Returns the users with superuser privileges.
     * @return the superusers.
     */
    public function getSuperusers() {
        $assignments = $this->_authManager->getAssignmentsByItemName(Rights::module()->superuserName);

        $userIdList = array();
        foreach ($assignments as $userId => $assignment)
            $userIdList[] = $userId;

        $criteria = new CDbCriteria();
        $criteria->addInCondition(Rights::module()->userIdColumn, $userIdList);

        $userClass = Rights::module()->userClass;
        $users = CActiveRecord::model($userClass)->findAll($criteria);
        $users = $this->attachUserBehavior($users);

        $superusers = array();
        foreach ($users as $user)
            $superusers[] = $user->name;

        // Make sure that we have superusers, otherwise we would allow full access to Rights
        // if there for some reason is not any superusers.
        if ($superusers === array())
            throw new CHttpException(403, Rights::t('core', 'There must be at least one superuser!'));

        return $superusers;
    }

    /**
     * Attaches the rights user behavior to the given users.
     * @param mixed $users the user or users to which attach the behavior.
     * @return mixed the user or users with the behavior attached.
     */
    public function attachUserBehavior($users) {
        $userClass = Rights::module()->userClass;

        // We have a single user.
        if ($users instanceof $userClass) {
            $users->attachBehavior('rights', new RUserBehavior);
        }
        // We have multiple user.
        else if ($users === (array) $users) {
            foreach ($users as $user)
                $user->attachBehavior('rights', new RUserBehavior);
        }

        return $users;
    }

    /**
     * Returns whether the user is a superuser.
     * @param integer $userId the id of the user to do the check for.
     * @return boolean whether the user is a superuser.
     */
    public function isSuperuser($userId) {
        $assignments = $this->_authManager->getAuthAssignments($userId);
        return isset($assignments[$this->superuserName]);
    }

    /**
     * Returns the permissions for a specific authorization item.
     * @param string $itemName the name of the item for which to get permissions. Defaults to null,
     * meaning that the full permission tree is returned.
     * @return the permission tree.
     */
    public function getPermissions($itemName = null) {
        $permissions = array();

        if ($itemName !== null) {
            $item = $this->_authManager->getAuthItem($itemName);
            $permissions = $this->getPermissionsRecursive($item);
        } else {
            foreach ($this->getRoles() as $roleName => $role) {
                $permissions[$roleName] = $this->getPermissionsRecursive($role);
            }
        }

        return $permissions;
    }

    /**
     * Returns the permissions for a specific authorization item recursively.
     * @param CAuthItem $item the item for which to get permissions.
     * @return array the section of the permissions tree.
     */
    private function getPermissionsRecursive(CAuthItem $item) {
        $permissions = array();
        foreach ($item->getChildren() as $childName => $child) {
            $permissions[$childName] = array();
            if (($grandChildren = $this->getPermissionsRecursive($child)) !== array())
                $permissions[$childName] = $grandChildren;
        }

        return $permissions;
    }

    /**
     * Returns the permissions for a specific authorization item.
     * @param string $itemName the name of the item for which to get permissions. Defaults to null,
     * meaning that the full permission tree is returned.
     * @return the permission tree.
     */
    public function getPermissionsWithBizRule($itemName = null) {
        $permissions = array();

        if ($itemName !== null) {
            $item = $this->_authManager->getAuthItem($itemName);
            $permissions = $this->getPermissionsWithBizRueRecursive($item);
        } else {
            foreach ($this->getRoles() as $roleName => $role) {
                $permissions[$roleName]['items'] = $this->getPermissionsWithBizRueRecursive($role);
                $permissions[$roleName]['bizRule'] = $role->bizRule;
            }
        }

        return $permissions;
    }

    /**
     * Returns the permissions for a specific authorization item recursively.
     * @param CAuthItem $item the item for which to get permissions.
     * @return array the section of the permissions tree.
     */
    private function getPermissionsWithBizRueRecursive(CAuthItem $item) {
        $permissions = array();
        foreach ($item->getChildren() as $childName => $child) {
            $permissions[$childName]['items'] = array();
            if (($grandChildren = $this->getPermissionsRecursive($child)) !== array())
                $permissions[$childName]['items'] = $grandChildren;
            $permissions['bizRule'] = $child->bizRule;
        }

        return $permissions;
    }

    /**
     * Returns the permission type for an authorization item.
     * @param string $itemName the name of the item to check permission for.
     * @param string $parentName the name of the item in which permissions to look.
     * @param array $permissions the permissions.
     * @return integer the permission type (0: None, 1: Direct, 2: Inherited).
     */
    public function hasPermission($itemName, $parentName = null, $permissions = array()) {
        if ($parentName !== null) {
            if ($parentName === $this->superuserName)
                return 1;

            $permissions = $this->getPermissions($parentName);
        }

        if (isset($permissions[$itemName]))
            return 1;

        foreach ($permissions as $children)
            if ($children !== array())
                if ($this->hasPermission($itemName, null, $children) > 0)
                    return 2;

        return 0;
    }

    /**
     * Tries to sanitize code to make it safe for execution.
     * @param string $code the code to be execute.
     * @return mixed the return value of eval() or null if the code was unsafe to execute.
     */
    protected function sanitizeExpression($code) {
        // Language consturcts.
        $languageConstructs = array(
            'echo',
            'empty',
            'isset',
            'unset',
            'exit',
            'die',
            'include',
            'include_once',
            'require',
            'require_once',
        );

        // Loop through the language constructs.
        foreach ($languageConstructs as $lc)
            if (preg_match('/' . $lc . '\ *\(?\ *[\"\']+/', $code) > 0)
                return null; // Language construct found, not safe for eval.


                
// Get a list of all defined functions
        $definedFunctions = get_defined_functions();
        $functions = array_merge($definedFunctions['internal'], $definedFunctions['user']);

        // Loop through the functions and check the code for function calls.
        // Append a '(' to the functions to avoid confusion between e.g. array() and array_merge().
        foreach ($functions as $f)
            if (preg_match('/' . $f . '\ *\({1}/', $code) > 0)
                return null; // Function call found, not safe for eval.


                
// Evaluate the safer code
        $result = @eval($code);

        // Return the evaluated code or null if the result was false.
        return $result !== false ? $result : null;
    }

    /**
     * @return RAuthManager the authorization manager.
     */
    public function getAuthManager() {
        return $this->_authManager;
    }

}
